home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / lib / python2.6 / dist-packages / apport / report.pyc (.txt) < prev    next >
Encoding:
Python Compiled Bytecode  |  2009-10-12  |  60.3 KB  |  1,779 lines

  1. # Source Generated with Decompyle++
  2. # File: in.pyc (Python 2.6)
  3.  
  4. '''Class for an apport report with some useful methods to collect standard
  5. debug information.
  6.  
  7. Copyright (C) 2006 Canonical Ltd.
  8. Author: Martin Pitt <martin.pitt@ubuntu.com>
  9.  
  10. This program is free software; you can redistribute it and/or modify it
  11. under the terms of the GNU General Public License as published by the
  12. Free Software Foundation; either version 2 of the License, or (at your
  13. option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
  14. the full text of the license.
  15. '''
  16. import subprocess
  17. import tempfile
  18. import os.path as os
  19. import urllib
  20. import re
  21. import pwd
  22. import grp
  23. import os
  24. import sys
  25. import fnmatch
  26. import glob
  27. import atexit
  28. import traceback
  29. import xml.dom as xml
  30. import xml.dom.minidom as xml
  31. from xml.parsers.expat import ExpatError
  32. from problem_report import ProblemReport
  33. import fileutils
  34. from packaging_impl import impl as packaging
  35. _hook_dir = '/usr/share/apport/package-hooks/'
  36. _common_hook_dir = '/usr/share/apport/general-hooks/'
  37. _ignore_file = '~/.apport-ignore.xml'
  38. _blacklist_dir = '/etc/apport/blacklist.d'
  39. interpreters = [
  40.     'sh',
  41.     'bash',
  42.     'dash',
  43.     'csh',
  44.     'tcsh',
  45.     'python*',
  46.     'ruby*',
  47.     'php',
  48.     'perl*',
  49.     'mono*',
  50.     'awk']
  51.  
  52. def _transitive_dependencies(package, depends_set):
  53.     '''Recursively add dependencies of package to depends_set.'''
  54.     
  55.     try:
  56.         cur_ver = packaging.get_version(package)
  57.     except ValueError:
  58.         return None
  59.  
  60.     for d in packaging.get_dependencies(package):
  61.         if d not in depends_set:
  62.             depends_set.add(d)
  63.             _transitive_dependencies(d, depends_set)
  64.             continue
  65.     
  66.  
  67.  
  68. def _read_file(f):
  69.     '''Try to read given file and return its contents, or return a textual
  70.     error if it failed.'''
  71.     
  72.     try:
  73.         return open(f).read().strip()
  74.     except (OSError, IOError):
  75.         e = None
  76.         return 'Error: ' + str(e)
  77.  
  78.  
  79.  
  80. def _read_maps(pid):
  81.     '''
  82.     Since /proc/$pid/maps may become unreadable unless we are
  83.     ptracing the process, detect this, and attempt to attach/detach
  84.     '''
  85.     maps = 'Error: unable to read /proc maps file'
  86.     
  87.     try:
  88.         maps = file('/proc/%d/maps' % pid).read().strip()
  89.     except (OSError, IOError):
  90.         e = None
  91.         return 'Error: ' + str(e)
  92.  
  93.     return maps
  94.  
  95.  
  96. def _command_output(command, input = None, stderr = subprocess.STDOUT):
  97.     '''Try to execute given command (array) and return its stdout, or return
  98.     a textual error if it failed.'''
  99.     sp = subprocess.Popen(command, stdout = subprocess.PIPE, stderr = stderr, close_fds = True)
  100.     (out, err) = sp.communicate(input)
  101.     if sp.returncode == 0:
  102.         return out
  103.     raise OSError, 'Error: command %s failed with exit code %i: %s' % (str(command), sp.returncode, err)
  104.  
  105.  
  106. def _check_bug_pattern(report, pattern):
  107.     '''Check if given report matches the given bug pattern XML DOM node; return the
  108.     bug URL on match, otherwise None.'''
  109.     if not pattern.attributes.has_key('url'):
  110.         return None
  111.     for c in pattern.childNodes:
  112.         if c.nodeType == xml.dom.Node.ELEMENT_NODE and c.nodeName == 're' and c.attributes.has_key('key'):
  113.             key = c.attributes['key'].nodeValue
  114.             if not report.has_key(key):
  115.                 return None
  116.             c.normalize()
  117.             if c.hasChildNodes() and c.childNodes[0].nodeType == xml.dom.Node.TEXT_NODE:
  118.                 regexp = c.childNodes[0].nodeValue.encode('UTF-8')
  119.                 
  120.                 try:
  121.                     if not re.search(regexp, report[key]):
  122.                         return None
  123.                 pattern.attributes.has_key('url')
  124.                 return None
  125.  
  126.             
  127.         c.childNodes[0].nodeType == xml.dom.Node.TEXT_NODE
  128.     
  129.     return pattern.attributes['url'].nodeValue.encode('UTF-8')
  130.  
  131.  
  132. def _dom_remove_space(node):
  133.     '''Recursively remove whitespace from given XML DOM node.'''
  134.     for c in node.childNodes:
  135.         if c.nodeType == xml.dom.Node.TEXT_NODE and c.nodeValue.strip() == '':
  136.             c.unlink()
  137.             node.removeChild(c)
  138.             continue
  139.         _dom_remove_space(c)
  140.     
  141.  
  142.  
  143. def get_module_license(module):
  144.     '''Return the license for a given kernel module.'''
  145.     
  146.     try:
  147.         modinfo = subprocess.Popen([
  148.             '/sbin/modinfo',
  149.             module], stdout = subprocess.PIPE)
  150.         out = modinfo.communicate()[0]
  151.         if modinfo.returncode != 0:
  152.             return None
  153.     except OSError:
  154.         return None
  155.  
  156.     for l in out.splitlines():
  157.         fields = l.split(':', 1)
  158.         if len(fields) < 2:
  159.             continue
  160.         
  161.         if fields[0] == 'license':
  162.             return fields[1].strip()
  163.     
  164.  
  165.  
  166. def nonfree_modules(module_list = '/proc/modules'):
  167.     '''Check loaded modules and return a list of those which are not free.'''
  168.     
  169.     try:
  170.         mods = [ l.split()[0] for l in open(module_list) ]
  171.     except IOError:
  172.         return []
  173.  
  174.     nonfree = []
  175.     for m in mods:
  176.         l = get_module_license(m)
  177.         if l:
  178.             if not 'GPL' in l and 'BSD' in l and 'MPL' in l:
  179.                 pass
  180.             if not ('MIT' in l):
  181.                 nonfree.append(m)
  182.                 continue
  183.     
  184.     return nonfree
  185.  
  186.  
  187. class Report(ProblemReport):
  188.     '''A problem report specific to apport (crash or bug).
  189.  
  190.     This class wraps a standard ProblemReport and adds methods for collecting
  191.     standard debugging data.'''
  192.     
  193.     def __init__(self, type = 'Crash', date = None):
  194.         '''Initialize a fresh problem report.
  195.  
  196.            date is the desired date/time string; if None (default), the current
  197.            local time is used.
  198.            '''
  199.         ProblemReport.__init__(self, type, date)
  200.  
  201.     
  202.     def _pkg_modified_suffix(self, package):
  203.         """Return a string suitable for appending to Package:/Dependencies:
  204.         fields.
  205.  
  206.         If package has only unmodified files, return the empty string. If not,
  207.         return ' [modified: ...]' with a list of modified files."""
  208.         mod = packaging.get_modified_files(package)
  209.         if mod:
  210.             return ' [modified: %s]' % ' '.join(mod)
  211.         return ''
  212.  
  213.     
  214.     def add_package_info(self, package = None):
  215.         '''Add packaging information.
  216.  
  217.         If package is not given, the report must have ExecutablePath.
  218.         This adds:
  219.         - Package: package name and installed version
  220.         - SourcePackage: source package name
  221.         - PackageArchitecture: processor architecture this package was built
  222.           for
  223.         - Dependencies: package names and versions of all dependencies and
  224.           pre-dependencies; this also checks if the files are unmodified and
  225.           appends a list of all modified files'''
  226.         if not package:
  227.             package = fileutils.find_file_package(self['ExecutablePath'])
  228.             if not package:
  229.                 return None
  230.         
  231.         self['Package'] = '%s %s%s' % (package, packaging.get_version(package), self._pkg_modified_suffix(package))
  232.         self['SourcePackage'] = packaging.get_source(package)
  233.         self['PackageArchitecture'] = packaging.get_architecture(package)
  234.         dependencies = set([])
  235.         _transitive_dependencies(package, dependencies)
  236.         self['Dependencies'] = ''
  237.         for dep in dependencies:
  238.             
  239.             try:
  240.                 v = packaging.get_version(dep)
  241.             except ValueError:
  242.                 continue
  243.  
  244.             if self['Dependencies']:
  245.                 self['Dependencies'] += '\n'
  246.             
  247.             self['Dependencies'] += '%s %s%s' % (dep, v, self._pkg_modified_suffix(dep))
  248.         
  249.  
  250.     
  251.     def add_os_info(self):
  252.         '''Add operating system information.
  253.  
  254.         This adds:
  255.         - DistroRelease: lsb_release -sir output
  256.         - Architecture: system architecture in distro specific notation
  257.         - Uname: uname -srm output
  258.         - NonfreeKernelModules: loaded kernel modules which are not free (if
  259.             there are none, this field will not be present)'''
  260.         p = subprocess.Popen([
  261.             'lsb_release',
  262.             '-sir'], stdout = subprocess.PIPE, stderr = subprocess.PIPE, close_fds = True)
  263.         self['DistroRelease'] = p.communicate()[0].strip().replace('\n', ' ')
  264.         u = os.uname()
  265.         self['Uname'] = '%s %s %s' % (u[0], u[2], u[4])
  266.         self['Architecture'] = packaging.get_system_architecture()
  267.         nm = nonfree_modules()
  268.         if nm:
  269.             self['NonfreeKernelModules'] = ' '.join(nonfree_modules())
  270.         
  271.  
  272.     
  273.     def add_user_info(self):
  274.         '''Add information about the user.
  275.  
  276.         This adds:
  277.         - UserGroups: system groups the user is in
  278.         '''
  279.         user = pwd.getpwuid(os.getuid()).pw_name
  280.         groups = _[1]
  281.         groups.sort()
  282.         self['UserGroups'] = ' '.join(groups)
  283.  
  284.     
  285.     def _check_interpreted(self):
  286.         '''Check ExecutablePath, ProcStatus and ProcCmdline if the process is
  287.         interpreted.'''
  288.         if not self.has_key('ExecutablePath'):
  289.             return None
  290.         exebasename = os.path.basename(self['ExecutablePath'])
  291.         if not (filter,)((lambda i: fnmatch.fnmatch(exebasename, i)), interpreters):
  292.             return None
  293.         name = None
  294.         for l in self['ProcStatus'].splitlines():
  295.             
  296.             try:
  297.                 (k, v) = l.split('\t', 1)
  298.             except ValueError:
  299.                 (filter,)((lambda i: fnmatch.fnmatch(exebasename, i)), interpreters)
  300.                 (filter,)((lambda i: fnmatch.fnmatch(exebasename, i)), interpreters)
  301.                 self.has_key('ExecutablePath')
  302.                 continue
  303.             except:
  304.                 (filter,)((lambda i: fnmatch.fnmatch(exebasename, i)), interpreters)
  305.  
  306.             if k == 'Name:':
  307.                 name = v
  308.                 break
  309.                 continue
  310.             (filter,)((lambda i: fnmatch.fnmatch(exebasename, i)), interpreters)
  311.         
  312.         if not name:
  313.             return None
  314.         cmdargs = self['ProcCmdline'].split('\x00')
  315.         bindirs = [
  316.             '/bin/',
  317.             '/sbin/',
  318.             '/usr/bin/',
  319.             '/usr/sbin/']
  320.         while len(cmdargs) >= 2 and cmdargs[1].startswith('-'):
  321.             del cmdargs[1]
  322.             continue
  323.             name
  324.         if len(cmdargs) >= 2:
  325.             if cmdargs[1].startswith('.') and self.has_key('ProcCwd'):
  326.                 cmdargs[1] = os.path.join(self['ProcCwd'], cmdargs[1])
  327.             
  328.             if os.access(cmdargs[1], os.R_OK):
  329.                 self['InterpreterPath'] = self['ExecutablePath']
  330.                 self['ExecutablePath'] = os.path.realpath(cmdargs[1])
  331.                 return None
  332.         
  333.  
  334.     
  335.     def add_proc_info(self, pid = None, extraenv = []):
  336.         """Add /proc/pid information.
  337.  
  338.         If pid is not given, it defaults to the process' current pid.
  339.  
  340.         This adds the following fields:
  341.         - ExecutablePath: /proc/pid/exe contents; if the crashed process is
  342.           interpreted, this contains the script path instead
  343.         - InterpreterPath: /proc/pid/exe contents if the crashed process is
  344.           interpreted; otherwise this key does not exist
  345.         - ProcEnviron: A subset of the process' environment (only some standard
  346.           variables that do not disclose potentially sensitive information, plus
  347.           the ones mentioned in extraenv)
  348.         - ProcCmdline: /proc/pid/cmdline contents
  349.         - ProcStatus: /proc/pid/status contents
  350.         - ProcMaps: /proc/pid/maps contents
  351.         - ProcAttrCurrent: /proc/pid/attr/current contents"""
  352.         if not pid:
  353.             pid = os.getpid()
  354.         
  355.         pid = str(pid)
  356.         
  357.         try:
  358.             self['ProcCwd'] = os.readlink('/proc/' + pid + '/cwd')
  359.         except OSError:
  360.             pass
  361.  
  362.         self.add_proc_environ(pid, extraenv)
  363.         self['ProcStatus'] = _read_file('/proc/' + pid + '/status')
  364.         self['ProcCmdline'] = _read_file('/proc/' + pid + '/cmdline').rstrip('\x00')
  365.         self['ProcMaps'] = _read_maps(int(pid))
  366.         self['ExecutablePath'] = os.readlink('/proc/' + pid + '/exe')
  367.         for p in ('rofs', 'rwfs', 'squashmnt', 'persistmnt'):
  368.             if self['ExecutablePath'].startswith('/%s/' % p):
  369.                 self['ExecutablePath'] = self['ExecutablePath'][len('/%s' % p):]
  370.                 break
  371.                 continue
  372.         
  373.         if not os.path.exists(self['ExecutablePath']):
  374.             raise AssertionError
  375.         self._check_interpreted()
  376.         self['ProcCmdline'] = self['ProcCmdline'].replace('\\', '\\\\').replace(' ', '\\ ').replace('\x00', ' ')
  377.         
  378.         try:
  379.             if os.getuid() == 0:
  380.                 self['ProcAttrCurrent'] = open('/proc/' + pid + '/attr/current').read().strip()
  381.         except (IOError, OSError):
  382.             os.path.exists(self['ExecutablePath'])
  383.             os.path.exists(self['ExecutablePath'])
  384.         except:
  385.             os.path.exists(self['ExecutablePath'])
  386.  
  387.  
  388.     
  389.     def add_proc_environ(self, pid = None, extraenv = []):
  390.         """Add environment information.
  391.  
  392.         If pid is not given, it defaults to the process' current pid.
  393.  
  394.         This adds the following fields:
  395.         - ProcEnviron: A subset of the process' environment (only some standard
  396.           variables that do not disclose potentially sensitive information, plus
  397.           the ones mentioned in extraenv)
  398.         """
  399.         safe_vars = [
  400.             'SHELL',
  401.             'LANGUAGE',
  402.             'LANG',
  403.             'LC_CTYPE',
  404.             'LC_COLLATE',
  405.             'LC_TIME',
  406.             'LC_NUMERIC',
  407.             'LC_MONETARY',
  408.             'LC_MESSAGES',
  409.             'LC_PAPER',
  410.             'LC_NAME',
  411.             'LC_ADDRESS',
  412.             'LC_TELEPHONE',
  413.             'LC_MEASUREMENT',
  414.             'LC_IDENTIFICATION',
  415.             'LOCPATH'] + extraenv
  416.         if not pid:
  417.             pid = os.getpid()
  418.         
  419.         pid = str(pid)
  420.         self['ProcEnviron'] = ''
  421.         env = _read_file('/proc/' + pid + '/environ').replace('\n', '\\n')
  422.         if env.startswith('Error:'):
  423.             self['ProcEnviron'] = env
  424.         else:
  425.             for l in env.split('\x00'):
  426.                 if l.split('=', 1)[0] in safe_vars:
  427.                     if self['ProcEnviron']:
  428.                         self['ProcEnviron'] += '\n'
  429.                     
  430.                     self['ProcEnviron'] += l
  431.                     continue
  432.                 if l.startswith('PATH='):
  433.                     p = l.split('=', 1)[1]
  434.                     if '/home' in p or '/tmp' in p:
  435.                         if self['ProcEnviron']:
  436.                             self['ProcEnviron'] += '\n'
  437.                         
  438.                         self['ProcEnviron'] += 'PATH=(custom, user)'
  439.                     elif p != '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games':
  440.                         if self['ProcEnviron']:
  441.                             self['ProcEnviron'] += '\n'
  442.                         
  443.                         self['ProcEnviron'] += 'PATH=(custom, no user)'
  444.                     
  445.                 '/tmp' in p
  446.             
  447.  
  448.     
  449.     def add_gdb_info(self, debugdir = None):
  450.         """Add information from gdb.
  451.  
  452.         This requires that the report has a CoreDump and an
  453.         ExecutablePath. This adds the following fields:
  454.         - Registers: Output of gdb's 'info registers' command
  455.         - Disassembly: Output of gdb's 'x/16i $pc' command
  456.         - Stacktrace: Output of gdb's 'bt full' command
  457.         - ThreadStacktrace: Output of gdb's 'thread apply all bt full' command
  458.         - StacktraceTop: simplified stacktrace (topmost 5 functions) for inline
  459.           inclusion into bug reports and easier processing
  460.  
  461.         The optional debugdir can specify an alternative debug symbol root
  462.         directory.
  463.         """
  464.         if not self.has_key('CoreDump') or not self.has_key('ExecutablePath'):
  465.             return None
  466.         unlink_core = False
  467.         
  468.         try:
  469.             if hasattr(self['CoreDump'], 'find'):
  470.                 (fd, core) = tempfile.mkstemp()
  471.                 os.write(fd, self['CoreDump'])
  472.                 os.close(fd)
  473.                 unlink_core = True
  474.             elif hasattr(self['CoreDump'], 'gzipvalue'):
  475.                 (fd, core) = tempfile.mkstemp()
  476.                 os.close(fd)
  477.                 self['CoreDump'].write(open(core, 'w'))
  478.                 unlink_core = True
  479.             else:
  480.                 core = self['CoreDump'][0]
  481.             gdb_reports = {
  482.                 'Registers': 'info registers',
  483.                 'Disassembly': 'x/16i $pc',
  484.                 'Stacktrace': 'bt full',
  485.                 'ThreadStacktrace': 'thread apply all bt full' }
  486.             command = [
  487.                 'gdb',
  488.                 '--batch']
  489.             if debugdir:
  490.                 command += [
  491.                     '--ex',
  492.                     'set debug-file-directory ' + debugdir]
  493.             
  494.             command += [
  495.                 '--ex',
  496.                 'file ' + self.get('InterpreterPath', self['ExecutablePath']),
  497.                 '--ex',
  498.                 'core-file ' + core]
  499.             command += [
  500.                 '--ex',
  501.                 'set backtrace limit 2000']
  502.             value_keys = []
  503.             for name, cmd in gdb_reports.iteritems():
  504.                 value_keys.append(name)
  505.                 command += [
  506.                     '--ex',
  507.                     'p -99',
  508.                     '--ex',
  509.                     cmd]
  510.             
  511.             if not os.path.exists(self.get('InterpreterPath', self['ExecutablePath'])):
  512.                 raise AssertionError
  513.             
  514.             try:
  515.                 out = _command_output(command, stderr = open('/dev/null')).replace('(no debugging symbols found)\n', '').replace('No symbol table info available.\n', '')
  516.             except OSError:
  517.                 os.path.exists(self.get('InterpreterPath', self['ExecutablePath']))
  518.                 os.path.exists(self.get('InterpreterPath', self['ExecutablePath']))
  519.                 return None
  520.  
  521.             part_re = re.compile('^\\$\\d+\\s*=\\s*-99$', re.MULTILINE)
  522.             parts = part_re.split(out)
  523.             parts.pop(0)
  524.             for part in parts:
  525.                 self[value_keys.pop(0)] = part.replace('\n\n', '\n.\n').strip()
  526.         finally:
  527.             pass
  528.  
  529.         if self.has_key('Stacktrace'):
  530.             self._gen_stacktrace_top()
  531.         
  532.  
  533.     
  534.     def _gen_stacktrace_top(self):
  535.         '''Build field StacktraceTop as the top five functions of Stacktrace. 
  536.  
  537.         Signal handler invocations and related functions are skipped since they
  538.         are generally not useful for triaging and duplicate detection.'''
  539.         unwind_functions = set([
  540.             'g_logv',
  541.             'g_log',
  542.             'IA__g_log',
  543.             'IA__g_logv',
  544.             'g_assert_warning',
  545.             'IA__g_assert_warning'])
  546.         toptrace = [
  547.             ''] * 5
  548.         depth = 0
  549.         unwound = False
  550.         unwinding = False
  551.         bt_fn_re = re.compile('^#(\\d+)\\s+(?:0x(?:\\w+)\\s+in\\s+(.*)|(<signal handler called>)\\s*)$')
  552.         bt_fn_noaddr_re = re.compile('^#(\\d+)\\s+(?:(.*)|(<signal handler called>)\\s*)$')
  553.         for line in self['Stacktrace'].splitlines():
  554.             m = bt_fn_re.match(line)
  555.             if not m:
  556.                 m = bt_fn_noaddr_re.match(line)
  557.                 if not m:
  558.                     continue
  559.                 
  560.             
  561.             if not unwound or unwinding:
  562.                 if m.group(2):
  563.                     fn = m.group(2).split()[0].split('(')[0]
  564.                 else:
  565.                     fn = None
  566.                 if m.group(3) or fn in unwind_functions:
  567.                     unwinding = True
  568.                     depth = 0
  569.                     toptrace = [
  570.                         ''] * 5
  571.                     unwound = True
  572.                     continue
  573.                 else:
  574.                     unwinding = False
  575.             
  576.             if depth < len(toptrace):
  577.                 if not m.group(2):
  578.                     pass
  579.                 toptrace[depth] = m.group(3)
  580.                 depth += 1
  581.                 continue
  582.         
  583.         self['StacktraceTop'] = '\n'.join(toptrace).strip()
  584.  
  585.     
  586.     def add_hooks_info(self):
  587.         """Check for an existing hook script and run it to add additional
  588.         package specific information.
  589.  
  590.         A hook script needs to be in _hook_dir/<Package>.py or in
  591.         _common_hook_dir/*.py and has to contain a function 'add_info(report)'
  592.         that takes and modifies a Report."""
  593.         if 'Package' not in self:
  594.             return None
  595.         symb = { }
  596.         for hook in glob.glob(_common_hook_dir + '/*.py'):
  597.             
  598.             try:
  599.                 execfile(hook, symb)
  600.                 symb['add_info'](self)
  601.             continue
  602.             print >>sys.stderr, 'hook %s crashed:' % hook
  603.             traceback.print_exc()
  604.             continue
  605.  
  606.         
  607.         hook = '%s/%s.py' % (_hook_dir, self['Package'].split()[0])
  608.         if os.path.exists(hook):
  609.             
  610.             try:
  611.                 execfile(hook, symb)
  612.                 symb['add_info'](self)
  613.             print >>sys.stderr, 'hook %s crashed:' % hook
  614.             traceback.print_exc()
  615.  
  616.         
  617.         if self.has_key('SourcePackage'):
  618.             hook = '%s/source_%s.py' % (_hook_dir, self['SourcePackage'].split()[0])
  619.             if os.path.exists(hook):
  620.                 
  621.                 try:
  622.                     execfile(hook, symb)
  623.                     symb['add_info'](self)
  624.                 print >>sys.stderr, 'hook %s crashed:' % hook
  625.                 traceback.print_exc()
  626.  
  627.             
  628.         
  629.  
  630.     
  631.     def search_bug_patterns(self, baseurl):
  632.         '''Check bug patterns at baseurl/packagename.xml, return bug URL on match or
  633.         None otherwise.
  634.  
  635.         The pattern file must be valid XML and has the following syntax:
  636.         root element := <patterns>
  637.         patterns := <pattern url="http://bug.url"> *
  638.         pattern := <re key="report_key">regular expression*</re> +
  639.  
  640.         For example:
  641.         <?xml version="1.0"?>
  642.         <patterns>
  643.             <pattern url="http://bugtracker.net/bugs/1">
  644.                 <re key="Foo">ba.*r</re>
  645.             </pattern>
  646.             <pattern url="http://bugtracker.net/bugs/2">
  647.                 <re key="Foo">write_(hello|goodbye)</re>
  648.                 <re key="Package">^\\S* 1-2$</re> <!-- test for a particular version -->
  649.             </pattern>
  650.         </patterns>
  651.         '''
  652.         if not baseurl:
  653.             return None
  654.         if not self.has_key('Package'):
  655.             raise AssertionError
  656.         package = self['Package'].split()[0]
  657.         
  658.         try:
  659.             patterns = urllib.urlopen('%s/%s.xml' % (baseurl, package)).read()
  660.             if not '<title>404 Not Found' not in patterns:
  661.                 raise AssertionError
  662.         except:
  663.             self.has_key('Package')
  664.             baseurl
  665.             if self.has_key('SourcePackage'):
  666.                 
  667.                 try:
  668.                     patterns = urllib.urlopen('%s/%s.xml' % (baseurl, self['SourcePackage'])).read()
  669.                 return None
  670.  
  671.             else:
  672.                 return None
  673.  
  674.         
  675.         try:
  676.             dom = xml.dom.minidom.parseString(patterns)
  677.         except ExpatError:
  678.             self.has_key('Package')
  679.             self.has_key('Package')
  680.             baseurl
  681.             return None
  682.  
  683.         for pattern in dom.getElementsByTagName('pattern'):
  684.             m = _check_bug_pattern(self, pattern)
  685.             if m:
  686.                 return m
  687.         
  688.  
  689.     
  690.     def _get_ignore_dom(self):
  691.         '''Read ignore list XML file and return a DOM tree, or an empty DOM
  692.         tree if file does not exist.
  693.  
  694.         Raises ValueError if the file exists but is invalid XML.'''
  695.         ifpath = os.path.expanduser(_ignore_file)
  696.         if not os.access(ifpath, os.R_OK) or os.path.getsize(ifpath) == 0:
  697.             dom = xml.dom.getDOMImplementation().createDocument(None, 'apport', None)
  698.         else:
  699.             
  700.             try:
  701.                 dom = xml.dom.minidom.parse(ifpath)
  702.             except ExpatError:
  703.                 e = None
  704.                 raise ValueError, '%s has invalid format: %s' % (_ignore_file, str(e))
  705.  
  706.         dom.documentElement.normalize()
  707.         _dom_remove_space(dom.documentElement)
  708.         return dom
  709.  
  710.     
  711.     def check_ignored(self):
  712.         """Check ~/.apport-ignore.xml (in the real UID's home) and
  713.         /etc/apport/blacklist.d/ if the current report should not be presented
  714.         to the user.
  715.  
  716.         This requires the ExecutablePath attribute. Function can throw a
  717.         ValueError if the file has an invalid format."""
  718.         if not self.has_key('ExecutablePath'):
  719.             raise AssertionError
  720.         
  721.         try:
  722.             for f in os.listdir(_blacklist_dir):
  723.                 
  724.                 try:
  725.                     fd = open(os.path.join(_blacklist_dir, f))
  726.                 except IOError:
  727.                     self.has_key('ExecutablePath')
  728.                     self.has_key('ExecutablePath')
  729.                     continue
  730.                 except:
  731.                     self.has_key('ExecutablePath')
  732.  
  733.                 for line in fd:
  734.                     if line.strip() == self['ExecutablePath']:
  735.                         return True
  736.                 
  737.         except OSError:
  738.             self.has_key('ExecutablePath')
  739.             self.has_key('ExecutablePath')
  740.         except:
  741.             self.has_key('ExecutablePath')
  742.  
  743.         dom = self._get_ignore_dom()
  744.         
  745.         try:
  746.             cur_mtime = int(os.stat(self['ExecutablePath']).st_mtime)
  747.         except OSError:
  748.             self.has_key('ExecutablePath')
  749.             self.has_key('ExecutablePath')
  750.             return False
  751.  
  752.         for ignore in dom.getElementsByTagName('ignore'):
  753.             if ignore.getAttribute('program') == self['ExecutablePath']:
  754.                 if float(ignore.getAttribute('mtime')) >= cur_mtime:
  755.                     return True
  756.                 continue
  757.             float(ignore.getAttribute('mtime')) >= cur_mtime
  758.         
  759.         return False
  760.  
  761.     
  762.     def mark_ignore(self):
  763.         '''Add a ignore list entry for this report to ~/.apport-ignore.xml, so
  764.         that future reports for this ExecutablePath are not presented to the
  765.         user any more.
  766.  
  767.         Function can throw a ValueError if the file already exists and has an
  768.         invalid format.'''
  769.         if not self.has_key('ExecutablePath'):
  770.             raise AssertionError
  771.         dom = self._get_ignore_dom()
  772.         mtime = str(int(os.stat(self['ExecutablePath']).st_mtime))
  773.         for ignore in dom.getElementsByTagName('ignore'):
  774.             if ignore.getAttribute('program') == self['ExecutablePath']:
  775.                 ignore.setAttribute('mtime', mtime)
  776.                 break
  777.                 continue
  778.             self.has_key('ExecutablePath')
  779.         else:
  780.             e = dom.createElement('ignore')
  781.             e.setAttribute('mtime', mtime)
  782.             dom.documentElement.appendChild(e)
  783.         dom.writexml(open(os.path.expanduser(_ignore_file), 'w'), addindent = '  ', newl = '\n')
  784.         dom.unlink()
  785.  
  786.     
  787.     def has_useful_stacktrace(self):
  788.         """Check whether this report has a stacktrace that can be considered
  789.         'useful'.
  790.  
  791.         The current heuristic is to consider it useless if it either is shorter
  792.         than three lines and has any unknown function, or for longer traces, a
  793.         minority of known functions."""
  794.         if not self.get('StacktraceTop'):
  795.             return False
  796.         unknown_fn = [ f.startswith('??') for f in self['StacktraceTop'].splitlines() ]
  797.         if len(unknown_fn) < 3:
  798.             return unknown_fn.count(True) == 0
  799.         return unknown_fn.count(True) <= len(unknown_fn) / 2
  800.  
  801.     
  802.     def standard_title(self):
  803.         '''Create an appropriate title for a crash database entry.
  804.  
  805.         This contains the topmost function name from the stack trace and the
  806.         signal (for signal crashes) or the Python exception (for unhandled
  807.         Python exceptions).
  808.  
  809.         Return None if the report is not a crash or a default title could not
  810.         be generated.'''
  811.         if self.has_key('Signal') and self.has_key('ExecutablePath') and self.has_key('StacktraceTop'):
  812.             signal_names = {
  813.                 '4': 'SIGILL',
  814.                 '6': 'SIGABRT',
  815.                 '8': 'SIGFPE',
  816.                 '11': 'SIGSEGV',
  817.                 '13': 'SIGPIPE' }
  818.             fn = ''
  819.             for l in self['StacktraceTop'].splitlines():
  820.                 fname = l.split('(')[0].strip()
  821.                 if fname != '??':
  822.                     fn = ' in %s()' % fname
  823.                     break
  824.                     continue
  825.             
  826.             arch_mismatch = ''
  827.             if self.has_key('Architecture') and self.has_key('PackageArchitecture') and self['Architecture'] != self['PackageArchitecture'] and self['PackageArchitecture'] != 'all':
  828.                 arch_mismatch = ' [non-native %s package]' % self['PackageArchitecture']
  829.             
  830.             return '%s crashed with %s%s%s' % (os.path.basename(self['ExecutablePath']), signal_names.get(self.get('Signal'), 'signal ' + self.get('Signal')), fn, arch_mismatch)
  831.         if self.has_key('Traceback') and self.has_key('ExecutablePath'):
  832.             trace = self['Traceback'].splitlines()
  833.             if len(trace) < 1:
  834.                 return None
  835.             if len(trace) < 3:
  836.                 return '%s crashed with %s' % (os.path.basename(self['ExecutablePath']), trace[0])
  837.             trace_re = re.compile('^\\s*File.* in (.+)$')
  838.             i = len(trace) - 1
  839.             function = 'unknown'
  840.             while i >= 0:
  841.                 m = trace_re.match(trace[i])
  842.                 i -= 1
  843.                 continue
  844.                 self.has_key('StacktraceTop') if m else len(trace) < 1
  845.             return '%s crashed with %s in %s()' % (os.path.basename(self['ExecutablePath']), trace[-1].split(':')[0], function)
  846.         if self.get('ProblemType') == 'Package' and self.has_key('Package'):
  847.             title = 'package %s failed to install/upgrade' % self['Package']
  848.             return title
  849.         if self.get('ProblemType') == 'KernelOops' and self.has_key('OopsText'):
  850.             oops = self['OopsText']
  851.             return title
  852.         if self.get('ProblemType') == 'KernelOops' and self.has_key('Failure'):
  853.             title = ''
  854.             title += self['Failure'] + ' failure'
  855.             title += '\n'
  856.             return title
  857.  
  858.     
  859.     def obsolete_packages(self):
  860.         '''Check Package: and Dependencies: for obsolete packages and return a
  861.         list of them.'''
  862.         obsolete = []
  863.         for l in (self['Package'] + '\n' + self.get('Dependencies', '')).splitlines():
  864.             if not l:
  865.                 continue
  866.             
  867.             (pkg, ver) = l.split()[:2]
  868.             avail = packaging.get_available_version(pkg)
  869.             if ver != None and ver != 'None' and avail != None and packaging.compare_versions(ver, avail) < 0:
  870.                 obsolete.append(pkg)
  871.                 continue
  872.         
  873.         return obsolete
  874.  
  875.     
  876.     def crash_signature(self):
  877.         '''Calculate a signature string for a crash suitable for identifying
  878.         duplicates.
  879.  
  880.         For signal crashes this the concatenation of ExecutablePath, Signal
  881.         number, and StacktraceTop function names, separated by a colon. If
  882.         StacktraceTop has unknown functions or the report lacks any of those
  883.         fields, return None.
  884.         
  885.         For Python crashes, this concatenates the ExecutablePath, exception
  886.         name, and Traceback function names, again separated by a colon.'''
  887.         if not self.has_key('ExecutablePath'):
  888.             return None
  889.         if self.has_key('StacktraceTop') and self.has_key('Signal'):
  890.             sig = '%s:%s' % (self['ExecutablePath'], self['Signal'])
  891.             bt_fn_re = re.compile('^(?:([\\w:~]+).*|(<signal handler called>)\\s*)$')
  892.             for line in self['StacktraceTop'].splitlines():
  893.                 m = bt_fn_re.match(line)
  894.                 if m:
  895.                     if not m.group(1):
  896.                         pass
  897.                     sig += ':' + m.group(2)
  898.                     continue
  899.                 self.has_key('ExecutablePath')
  900.                 return None
  901.             
  902.             return sig
  903.         if self.has_key('Traceback'):
  904.             trace = self['Traceback'].splitlines()
  905.             sig = ''
  906.             if len(trace) == 1:
  907.                 m = re.match('(\\w+): ', trace[0])
  908.                 if m:
  909.                     return self['ExecutablePath'] + ':' + m.group(1)
  910.                 return None
  911.             len(trace) == 1
  912.             if len(trace) < 3:
  913.                 return None
  914.             for l in trace:
  915.                 if l.startswith('  File'):
  916.                     sig += ':' + l.split()[-1]
  917.                     continue
  918.                 len(trace) < 3
  919.             
  920.             return self['ExecutablePath'] + ':' + trace[-1].split(':')[0] + sig
  921.  
  922.     
  923.     def anonymize(self):
  924.         '''Remove user identifying strings from the report.
  925.  
  926.         This particularly removes the user name, host name, and IPs
  927.         from attributes which contain data read from the environment, and
  928.         removes the ProcCwd attribute completely.
  929.         '''
  930.         replacements = { }
  931.         if os.getuid() > 0:
  932.             p = pwd.getpwuid(os.getuid())
  933.             if len(p[0]) >= 2:
  934.                 replacements[p[0]] = 'username'
  935.             
  936.             replacements[p[5]] = '/home/username'
  937.             for s in p[4].split(','):
  938.                 s = s.strip()
  939.                 if len(s) > 2:
  940.                     replacements[s] = 'User Name'
  941.                     continue
  942.             
  943.         
  944.         hostname = os.uname()[1]
  945.         if len(hostname) >= 2:
  946.             replacements[hostname] = 'hostname'
  947.         
  948.         
  949.         try:
  950.             del self['ProcCwd']
  951.         except KeyError:
  952.             pass
  953.  
  954.         for k in self:
  955.             if k.startswith('Proc') and 'Stacktrace' in k or k in ('Traceback', 'PythonArgs'):
  956.                 for old, new in replacements.iteritems():
  957.                     if hasattr(self[k], 'isspace'):
  958.                         self[k] = self[k].replace(old, new)
  959.                         continue
  960.                 
  961.         
  962.  
  963.  
  964. import unittest
  965. import shutil
  966. import signal
  967. import time
  968. from cStringIO import StringIO
  969.  
  970. class _ApportReportTest(unittest.TestCase):
  971.     
  972.     def test_add_package_info(self):
  973.         '''add_package_info().'''
  974.         bashversion = packaging.get_version('bash')
  975.         pr = Report()
  976.         self.assertRaises(ValueError, pr.add_package_info, 'nonexistant_package')
  977.         pr.add_package_info('bash')
  978.         self.assertEqual(pr['Package'], 'bash ' + bashversion.strip())
  979.         self.assertEqual(pr['SourcePackage'], 'bash')
  980.         self.assert_('libc' in pr['Dependencies'])
  981.         pr = Report()
  982.         self.assertRaises(KeyError, pr.add_package_info)
  983.         pr['ExecutablePath'] = '/bin/bash'
  984.         pr.add_package_info()
  985.         self.assertEqual(pr['Package'], 'bash ' + bashversion.strip())
  986.         self.assertEqual(pr['SourcePackage'], 'bash')
  987.         self.assert_('libc' in pr['Dependencies'])
  988.         self.assert_('\n\n' not in pr['Dependencies'])
  989.         self.assert_(pr.has_key('PackageArchitecture'))
  990.         pr = Report()
  991.         pr['ExecutablePath'] = '/nonexisting'
  992.         pr.add_package_info()
  993.         self.assert_(not pr.has_key('Package'))
  994.  
  995.     
  996.     def test_add_os_info(self):
  997.         '''add_os_info().'''
  998.         pr = Report()
  999.         pr.add_os_info()
  1000.         self.assert_(pr['Uname'].startswith('Linux'))
  1001.         self.assert_(type(pr['DistroRelease']) == type(''))
  1002.         self.assert_(pr['Architecture'])
  1003.  
  1004.     
  1005.     def test_add_user_info(self):
  1006.         '''add_user_info().'''
  1007.         pr = Report()
  1008.         pr.add_user_info()
  1009.         self.assert_(pr.has_key('UserGroups'))
  1010.         for g in pr['UserGroups'].split():
  1011.             self.assert_(grp.getgrnam(g).gr_gid < 1000)
  1012.         
  1013.         self.assert_(grp.getgrgid(os.getgid()).gr_name not in pr['UserGroups'])
  1014.  
  1015.     
  1016.     def test_add_proc_info(self):
  1017.         '''add_proc_info().'''
  1018.         if not os.environ.has_key('LANG'):
  1019.             raise AssertionError, 'please set $LANG for this test'
  1020.         if not os.environ.has_key('USER'):
  1021.             raise AssertionError, 'please set $USER for this test'
  1022.         if not os.environ.has_key('PWD'):
  1023.             raise AssertionError, '$PWD is not set'
  1024.         pr = Report()
  1025.         pr.add_proc_info()
  1026.         self.assert_(set([
  1027.             'ProcEnviron',
  1028.             'ProcMaps',
  1029.             'ProcCmdline',
  1030.             'ProcMaps']).issubset(set(pr.keys())), 'report has required fields')
  1031.         self.assert_('LANG=' + os.environ['LANG'] in pr['ProcEnviron'])
  1032.         self.assert_('USER' not in pr['ProcEnviron'])
  1033.         self.assert_('PWD' not in pr['ProcEnviron'])
  1034.         pr = Report()
  1035.         pr.add_proc_info(extraenv = [
  1036.             'PWD'])
  1037.         self.assert_('USER' not in pr['ProcEnviron'])
  1038.         self.assert_('PWD=' + os.environ['PWD'] in pr['ProcEnviron'])
  1039.         if not os.getuid() != 0:
  1040.             raise AssertionError, 'please do not run this test as root for this check.'
  1041.         pr = Report()
  1042.         self.assertRaises(OSError, pr.add_proc_info, 1)
  1043.         self.assert_('init' in pr['ProcStatus'], pr['ProcStatus'])
  1044.         self.assert_(pr['ProcEnviron'].startswith('Error:'), pr['ProcEnviron'])
  1045.         self.assert_(not pr.has_key('InterpreterPath'))
  1046.         p = subprocess.Popen([
  1047.             'cat',
  1048.             '/foo bar',
  1049.             '\\h',
  1050.             '\\ \\',
  1051.             '-'], stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE, close_fds = True)
  1052.         if not p.pid:
  1053.             raise AssertionError
  1054.         while not open('/proc/%i/cmdline' % p.pid).read():
  1055.             time.sleep(0.1)
  1056.             continue
  1057.             p.pid
  1058.         pr = Report()
  1059.         pr.add_proc_info(pid = p.pid)
  1060.         p.communicate('\n')
  1061.         self.assertEqual(pr['ProcCmdline'], 'cat /foo\\ bar \\\\h \\\\\\ \\\\ -')
  1062.         self.assertEqual(pr['ExecutablePath'], '/bin/cat')
  1063.         self.assert_(not pr.has_key('InterpreterPath'))
  1064.         self.assertTrue('/bin/cat' in pr['ProcMaps'])
  1065.         self.assertTrue('[stack]' in pr['ProcMaps'])
  1066.         if not os.path.islink('/bin/sh'):
  1067.             raise AssertionError, '/bin/sh needs to be a symlink for this test'
  1068.         p = subprocess.Popen([
  1069.             'sh'], stdin = subprocess.PIPE, close_fds = True)
  1070.         if not p.pid:
  1071.             raise AssertionError
  1072.         while not open('/proc/%i/cmdline' % p.pid).read():
  1073.             time.sleep(0.1)
  1074.             continue
  1075.             p.pid
  1076.         pr = Report()
  1077.         pr.add_proc_info(pid = p.pid)
  1078.         p.communicate('exit\n')
  1079.         self.failIf(pr.has_key('InterpreterPath'), pr.get('InterpreterPath'))
  1080.         self.assertEqual(pr['ExecutablePath'], os.path.realpath('/bin/sh'))
  1081.         p = subprocess.Popen([
  1082.             'zgrep',
  1083.             'foo'], stdin = subprocess.PIPE, close_fds = True)
  1084.         if not p.pid:
  1085.             raise AssertionError
  1086.         while not open('/proc/%i/cmdline' % p.pid).read():
  1087.             time.sleep(0.1)
  1088.             continue
  1089.             p.pid
  1090.         pr = Report()
  1091.         pr.add_proc_info(pid = p.pid)
  1092.         p.communicate('\n')
  1093.         self.assert_(pr['ExecutablePath'].endswith('bin/zgrep'))
  1094.         self.assertEqual(pr['InterpreterPath'], os.path.realpath(open(pr['ExecutablePath']).readline().strip()[2:]))
  1095.         self.assertTrue('[stack]' in pr['ProcMaps'])
  1096.         (fd, testscript) = tempfile.mkstemp()
  1097.         os.write(fd, '#!/usr/bin/python\nimport sys\nsys.stdin.readline()\n')
  1098.         os.close(fd)
  1099.         os.chmod(testscript, 493)
  1100.         p = subprocess.Popen([
  1101.             testscript], stdin = subprocess.PIPE, stderr = subprocess.PIPE, close_fds = True)
  1102.         if not p.pid:
  1103.             raise AssertionError
  1104.         while not open('/proc/%i/cmdline' % p.pid).read():
  1105.             time.sleep(0.1)
  1106.             continue
  1107.             p.pid
  1108.         pr = Report()
  1109.         pr.add_proc_info(pid = p.pid)
  1110.         p.communicate('\n')
  1111.         os.unlink(testscript)
  1112.         self.assertEqual(pr['ExecutablePath'], testscript)
  1113.         self.assert_('python' in pr['InterpreterPath'])
  1114.         self.assertTrue('python' in pr['ProcMaps'])
  1115.         self.assertTrue('[stack]' in pr['ProcMaps'])
  1116.         self.assertRaises(OSError, pr.add_proc_info, p.pid)
  1117.  
  1118.     
  1119.     def test_add_path_classification(self):
  1120.         '''classification of $PATH.'''
  1121.         p = subprocess.Popen([
  1122.             'cat'], stdin = subprocess.PIPE, env = {
  1123.             'PATH': '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games' })
  1124.         time.sleep(0.1)
  1125.         r = Report()
  1126.         r.add_proc_environ(pid = p.pid)
  1127.         p.communicate('')
  1128.         self.failIf('PATH' in r['ProcEnviron'], 'system default $PATH should be filtered out')
  1129.         p = subprocess.Popen([
  1130.             'cat'], stdin = subprocess.PIPE, env = {
  1131.             'PATH': '/usr/sbin:/usr/bin:/sbin:/bin' })
  1132.         time.sleep(0.1)
  1133.         r = Report()
  1134.         r.add_proc_environ(pid = p.pid)
  1135.         p.communicate('')
  1136.         self.assert_('PATH=(custom, no user)' in r['ProcEnviron'], 'PATH is customized without user paths')
  1137.         p = subprocess.Popen([
  1138.             'cat'], stdin = subprocess.PIPE, env = {
  1139.             'PATH': '/home/pitti:/usr/sbin:/usr/bin:/sbin:/bin' })
  1140.         time.sleep(0.1)
  1141.         r = Report()
  1142.         r.add_proc_environ(pid = p.pid)
  1143.         p.communicate('')
  1144.         self.assert_('PATH=(custom, user)' in r['ProcEnviron'], 'PATH is customized with user paths')
  1145.  
  1146.     
  1147.     def test_check_interpreted(self):
  1148.         '''_check_interpreted().'''
  1149.         f = tempfile.NamedTemporaryFile()
  1150.         pr = Report()
  1151.         pr['ExecutablePath'] = '/usr/bin/gedit'
  1152.         pr['ProcStatus'] = 'Name:\tgedit'
  1153.         pr['ProcCmdline'] = 'gedit\x00/' + f.name
  1154.         pr._check_interpreted()
  1155.         self.assertEqual(pr['ExecutablePath'], '/usr/bin/gedit')
  1156.         self.failIf(pr.has_key('InterpreterPath'))
  1157.         f.close()
  1158.         pr = Report()
  1159.         pr['ExecutablePath'] = '/bin/dash'
  1160.         pr['ProcStatus'] = 'Name:\tznonexisting'
  1161.         pr['ProcCmdline'] = 'nonexisting\x00/foo'
  1162.         pr._check_interpreted()
  1163.         self.assertEqual(pr['ExecutablePath'], '/bin/dash')
  1164.         self.failIf(pr.has_key('InterpreterPath'))
  1165.         pr = Report()
  1166.         pr['ExecutablePath'] = '/bin/dash'
  1167.         pr['ProcStatus'] = 'Name:\tzgrep'
  1168.         pr['ProcCmdline'] = '/bin/sh\x00/bin/zgrep\x00foo'
  1169.         pr._check_interpreted()
  1170.         self.assertEqual(pr['ExecutablePath'], '/bin/zgrep')
  1171.         self.assertEqual(pr['InterpreterPath'], '/bin/dash')
  1172.         pr = Report()
  1173.         pr['ExecutablePath'] = '/bin/dash'
  1174.         pr['ProcStatus'] = 'Name:\tdash'
  1175.         pr['ProcCmdline'] = '/bin/sh\x00/bin/zgrep\x00foo'
  1176.         pr._check_interpreted()
  1177.         self.assertEqual(pr['ExecutablePath'], '/bin/zgrep')
  1178.         self.assertEqual(pr['InterpreterPath'], '/bin/dash')
  1179.         pr = Report()
  1180.         pr['ExecutablePath'] = '/usr/bin/mono'
  1181.         pr['ProcStatus'] = 'Name:\tzgrep'
  1182.         pr['ProcCmdline'] = 'zgrep\x00--debug\x00/bin/zgrep'
  1183.         pr._check_interpreted()
  1184.         self.assertEqual(pr['ExecutablePath'], '/bin/zgrep')
  1185.         self.assertEqual(pr['InterpreterPath'], '/usr/bin/mono')
  1186.         pr = Report()
  1187.         pr['ExecutablePath'] = '/usr/bin/mono'
  1188.         pr['ProcStatus'] = 'Name:\tzgrep'
  1189.         pr['ProcCmdline'] = 'zgrep\x00/bin/zgrep'
  1190.         pr._check_interpreted()
  1191.         self.assertEqual(pr['ExecutablePath'], '/bin/zgrep')
  1192.         self.assertEqual(pr['InterpreterPath'], '/usr/bin/mono')
  1193.         pr = Report()
  1194.         pr['ExecutablePath'] = '/usr/bin/python'
  1195.         pr['ProcStatus'] = 'Name:\tznonexisting'
  1196.         pr['ProcCmdline'] = 'python\x00/etc/shadow'
  1197.         pr._check_interpreted()
  1198.         self.assertEqual(pr['ExecutablePath'], '/usr/bin/python')
  1199.         self.failIf(pr.has_key('InterpreterPath'))
  1200.         pr = Report()
  1201.         pr['ExecutablePath'] = '/usr/bin/python'
  1202.         pr['ProcStatus'] = 'Name:\tznonexisting'
  1203.         pr['ProcCmdline'] = 'python\x00/etc/passwd'
  1204.         pr._check_interpreted()
  1205.         self.assertEqual(pr['InterpreterPath'], '/usr/bin/python')
  1206.         self.assertEqual(pr['ExecutablePath'], '/etc/passwd')
  1207.         pr = Report()
  1208.         pr['ExecutablePath'] = '/usr/bin/python'
  1209.         pr['ProcStatus'] = 'Name:\tshadow'
  1210.         pr['ProcCmdline'] = '../etc/shadow'
  1211.         pr._check_interpreted()
  1212.         self.assertEqual(pr['ExecutablePath'], '/usr/bin/python')
  1213.         self.failIf(pr.has_key('InterpreterPath'))
  1214.         pr = Report()
  1215.         pr['ExecutablePath'] = '/usr/bin/python'
  1216.         pr['ProcStatus'] = 'Name:\tpasswd'
  1217.         pr['ProcCmdline'] = '../etc/passwd'
  1218.         pr._check_interpreted()
  1219.         self.assertEqual(pr['InterpreterPath'], '/usr/bin/python')
  1220.         self.assertEqual(pr['ExecutablePath'], '/bin/../etc/passwd')
  1221.         pr = Report()
  1222.         pr['ExecutablePath'] = '/usr/bin/python'
  1223.         pr['ProcStatus'] = 'Name:\tpython'
  1224.         pr['ProcCmdline'] = 'python'
  1225.         pr._check_interpreted()
  1226.         self.assertEqual(pr['ExecutablePath'], '/usr/bin/python')
  1227.         self.failIf(pr.has_key('InterpreterPath'))
  1228.         pr = Report()
  1229.         pr['ExecutablePath'] = '/usr/bin/python'
  1230.         pr['ProcStatus'] = 'Name:\tbash'
  1231.         pr['ProcCmdline'] = 'python\x00/bin/bash'
  1232.         pr._check_interpreted()
  1233.         self.assertEqual(pr['InterpreterPath'], '/usr/bin/python')
  1234.         self.assertEqual(pr['ExecutablePath'], '/bin/bash')
  1235.         pr = Report()
  1236.         pr['ExecutablePath'] = '/usr/bin/python'
  1237.         pr['ProcStatus'] = 'Name:\tbash'
  1238.         pr['ProcCmdline'] = 'python\x00-OO\x00/bin/bash'
  1239.         pr._check_interpreted()
  1240.         self.assertEqual(pr['InterpreterPath'], '/usr/bin/python')
  1241.         self.assertEqual(pr['ExecutablePath'], '/bin/bash')
  1242.  
  1243.     
  1244.     def _generate_sigsegv_report(klass, file = None):
  1245.         '''Create a test executable which will die with a SIGSEGV, generate a
  1246.         core dump for it, create a problem report with those two arguments
  1247.         (ExecutablePath and CoreDump) and call add_gdb_info().
  1248.  
  1249.         If file is given, the report is written into it. Return the Report.'''
  1250.         workdir = None
  1251.         orig_cwd = os.getcwd()
  1252.         pr = Report()
  1253.         
  1254.         try:
  1255.             workdir = tempfile.mkdtemp()
  1256.             atexit.register(shutil.rmtree, workdir)
  1257.             os.chdir(workdir)
  1258.             open('crash.c', 'w').write('\nint f(x) {\n    int* p = 0; *p = x;\n    return x+1;\n}\nint main() { return f(42); }\n')
  1259.             if not subprocess.call([
  1260.                 'gcc',
  1261.                 '-g',
  1262.                 'crash.c',
  1263.                 '-o',
  1264.                 'crash']) == 0:
  1265.                 raise AssertionError
  1266.             if not os.path.exists('crash'):
  1267.                 raise AssertionError
  1268.             subprocess.call([
  1269.                 'gdb',
  1270.                 '--batch',
  1271.                 '--ex',
  1272.                 'run',
  1273.                 '--ex',
  1274.                 'generate-core-file core',
  1275.                 './crash'], stdout = subprocess.PIPE)
  1276.             if not os.path.exists('core'):
  1277.                 raise AssertionError
  1278.             if not subprocess.call([
  1279.                 'readelf',
  1280.                 '-n',
  1281.                 'core'], stdout = subprocess.PIPE) == 0:
  1282.                 raise AssertionError
  1283.             pr['ExecutablePath'] = os.path.join(workdir, 'crash')
  1284.             pr['CoreDump'] = (os.path.join(workdir, 'core'),)
  1285.             pr['Signal'] = '11'
  1286.             pr.add_gdb_info()
  1287.             if file:
  1288.                 pr.write(file)
  1289.                 file.flush()
  1290.         finally:
  1291.             os.chdir(orig_cwd)
  1292.  
  1293.         return pr
  1294.  
  1295.     _generate_sigsegv_report = classmethod(_generate_sigsegv_report)
  1296.     
  1297.     def _validate_gdb_fields(self, pr):
  1298.         self.assert_(pr.has_key('Stacktrace'))
  1299.         self.assert_(pr.has_key('ThreadStacktrace'))
  1300.         self.assert_(pr.has_key('StacktraceTop'))
  1301.         self.assert_(pr.has_key('Registers'))
  1302.         self.assert_(pr.has_key('Disassembly'))
  1303.         self.assert_('(no debugging symbols found)' not in pr['Stacktrace'])
  1304.         self.assert_('Core was generated by' not in pr['Stacktrace'], pr['Stacktrace'])
  1305.         self.assert_(not re.match('(?s)(^|.*\\n)#0  [^\\n]+\\n#0  ', pr['Stacktrace']))
  1306.         self.assert_('#0  0x' in pr['Stacktrace'])
  1307.         self.assert_('#1  0x' in pr['Stacktrace'])
  1308.         self.assert_('#0  0x' in pr['ThreadStacktrace'])
  1309.         self.assert_('#1  0x' in pr['ThreadStacktrace'])
  1310.         self.assert_('Thread 1 (process' in pr['ThreadStacktrace'])
  1311.         self.assert_(len(pr['StacktraceTop'].splitlines()) <= 5)
  1312.  
  1313.     
  1314.     def test_add_gdb_info(self):
  1315.         '''add_gdb_info() with core dump file reference.'''
  1316.         pr = Report()
  1317.         pr.add_gdb_info()
  1318.         pr = self._generate_sigsegv_report()
  1319.         self._validate_gdb_fields(pr)
  1320.         self.assertEqual(pr['StacktraceTop'], 'f (x=42) at crash.c:3\nmain () at crash.c:6')
  1321.  
  1322.     
  1323.     def test_add_gdb_info_load(self):
  1324.         '''add_gdb_info() with inline core dump.'''
  1325.         rep = tempfile.NamedTemporaryFile()
  1326.         self._generate_sigsegv_report(rep)
  1327.         rep.seek(0)
  1328.         pr = Report()
  1329.         pr.load(open(rep.name))
  1330.         pr.add_gdb_info()
  1331.         self._validate_gdb_fields(pr)
  1332.  
  1333.     
  1334.     def test_add_gdb_info_script(self):
  1335.         '''add_gdb_info() with a script.'''
  1336.         (fd, coredump) = tempfile.mkstemp()
  1337.         (fd2, script) = tempfile.mkstemp()
  1338.         
  1339.         try:
  1340.             os.close(fd)
  1341.             os.close(fd2)
  1342.             open(script, 'w').write("#!/bin/bash\ngdb --batch --ex 'generate-core-file %s' --pid $$ >/dev/null" % coredump)
  1343.             os.chmod(script, 493)
  1344.             if not subprocess.call([
  1345.                 script]) == 0:
  1346.                 raise AssertionError
  1347.             if not subprocess.call([
  1348.                 'readelf',
  1349.                 '-n',
  1350.                 coredump], stdout = subprocess.PIPE) == 0:
  1351.                 raise AssertionError
  1352.             pr = Report()
  1353.             pr['InterpreterPath'] = '/bin/bash'
  1354.             pr['ExecutablePath'] = script
  1355.             pr['CoreDump'] = (coredump,)
  1356.             pr.add_gdb_info()
  1357.         finally:
  1358.             os.unlink(coredump)
  1359.             os.unlink(script)
  1360.  
  1361.         self._validate_gdb_fields(pr)
  1362.         if not 'libc.so' in pr['Stacktrace']:
  1363.             pass
  1364.         self.assert_('in execute_command' in pr['Stacktrace'])
  1365.  
  1366.     
  1367.     def test_search_bug_patterns(self):
  1368.         '''search_bug_patterns().'''
  1369.         pdir = None
  1370.         
  1371.         try:
  1372.             pdir = tempfile.mkdtemp()
  1373.             open(os.path.join(pdir, 'bash.xml'), 'w').write('<?xml version="1.0"?>\n<patterns>\n    <pattern url="http://bugtracker.net/bugs/1">\n        <re key="Foo">ba.*r</re>\n    </pattern>\n    <pattern url="http://bugtracker.net/bugs/2">\n        <re key="Foo">write_(hello|goodbye)</re>\n        <re key="Package">^\\S* 1-2$</re>\n    </pattern>\n</patterns>')
  1374.             open(os.path.join(pdir, 'coreutils.xml'), 'w').write('<?xml version="1.0"?>\n<patterns>\n    <pattern url="http://bugtracker.net/bugs/3">\n        <re key="Bar">^1$</re>\n    </pattern>\n    <pattern url="http://bugtracker.net/bugs/4">\n        <re key="Bar">*</re> <!-- invalid RE -->\n    </pattern>\n</patterns>')
  1375.             open(os.path.join(pdir, 'invalid.xml'), 'w').write('<?xml version="1.0"?>\n</patterns>')
  1376.             r_bash = Report()
  1377.             r_bash['Package'] = 'bash 1-2'
  1378.             r_bash['Foo'] = 'bazaar'
  1379.             r_coreutils = Report()
  1380.             r_coreutils['Package'] = 'coreutils 1'
  1381.             r_coreutils['Bar'] = '1'
  1382.             r_invalid = Report()
  1383.             r_invalid['Package'] = 'invalid 1'
  1384.             self.assertEqual(r_bash.search_bug_patterns(pdir), 'http://bugtracker.net/bugs/1')
  1385.             r_bash['Foo'] = 'write_goodbye'
  1386.             self.assertEqual(r_bash.search_bug_patterns(pdir), 'http://bugtracker.net/bugs/2')
  1387.             self.assertEqual(r_coreutils.search_bug_patterns(pdir), 'http://bugtracker.net/bugs/3')
  1388.             r_bash['Package'] = 'bash-static 1-2'
  1389.             self.assertEqual(r_bash.search_bug_patterns(pdir), None)
  1390.             r_bash['SourcePackage'] = 'bash'
  1391.             self.assertEqual(r_bash.search_bug_patterns(pdir), 'http://bugtracker.net/bugs/2')
  1392.             r_bash['Package'] = 'bash 1-21'
  1393.             self.assertEqual(r_bash.search_bug_patterns(pdir), None, 'does not match on wrong bash version')
  1394.             r_bash['Foo'] = 'zz'
  1395.             self.assertEqual(r_bash.search_bug_patterns(pdir), None, 'does not match on wrong Foo value')
  1396.             r_coreutils['Bar'] = '11'
  1397.             self.assertEqual(r_coreutils.search_bug_patterns(pdir), None, 'does not match on wrong Bar value')
  1398.             del r_coreutils['Bar']
  1399.             self.assertEqual(r_coreutils.search_bug_patterns(pdir), None, 'does not match on nonexisting key')
  1400.             self.assertEqual(r_invalid.search_bug_patterns(pdir), None, 'gracefully handles invalid XML')
  1401.             r_coreutils['Package'] = 'other 2'
  1402.             self.assertEqual(r_coreutils.search_bug_patterns(pdir), None, 'gracefully handles nonexisting package XML file')
  1403.             self.assertEqual(r_bash.search_bug_patterns('file:///nonexisting/directory/'), None, 'gracefully handles nonexisting base path')
  1404.             self.assertEqual(r_bash.search_bug_patterns('http://security.ubuntu.com/'), None, 'gracefully handles base path without bug patterns')
  1405.             self.assertEqual(r_bash.search_bug_patterns('http://nonexisting.domain/'), None, 'gracefully handles nonexisting URL domain')
  1406.         finally:
  1407.             if pdir:
  1408.                 shutil.rmtree(pdir)
  1409.             
  1410.  
  1411.  
  1412.     
  1413.     def test_add_hooks_info(self):
  1414.         '''add_hooks_info().'''
  1415.         global _hook_dir, _common_hook_dir, _hook_dir, _common_hook_dir
  1416.         orig_hook_dir = _hook_dir
  1417.         _hook_dir = tempfile.mkdtemp()
  1418.         orig_common_hook_dir = _common_hook_dir
  1419.         _common_hook_dir = tempfile.mkdtemp()
  1420.         
  1421.         try:
  1422.             open(os.path.join(_hook_dir, 'foo.py'), 'w').write("\ndef add_info(report):\n    report['Field1'] = 'Field 1'\n    report['Field2'] = 'Field 2\\nBla'\n")
  1423.             open(os.path.join(_common_hook_dir, 'foo1.py'), 'w').write("\ndef add_info(report):\n    report['CommonField1'] = 'CommonField 1'\n")
  1424.             open(os.path.join(_common_hook_dir, 'foo2.py'), 'w').write("\ndef add_info(report):\n    report['CommonField2'] = 'CommonField 2'\n")
  1425.             open(os.path.join(_common_hook_dir, 'notme'), 'w').write("\ndef add_info(report):\n    report['BadField'] = 'XXX'\n")
  1426.             r = Report()
  1427.             r['Package'] = 'bar'
  1428.             r.add_hooks_info()
  1429.             self.assertEqual(set(r.keys()), set([
  1430.                 'ProblemType',
  1431.                 'Date',
  1432.                 'Package',
  1433.                 'CommonField1',
  1434.                 'CommonField2']), 'report has required fields')
  1435.             r = Report()
  1436.             r['Package'] = 'baz 1.2-3'
  1437.             r.add_hooks_info()
  1438.             self.assertEqual(set(r.keys()), set([
  1439.                 'ProblemType',
  1440.                 'Date',
  1441.                 'Package',
  1442.                 'CommonField1',
  1443.                 'CommonField2']), 'report has required fields')
  1444.             r = Report()
  1445.             r['Package'] = 'foo'
  1446.             r.add_hooks_info()
  1447.             self.assertEqual(set(r.keys()), set([
  1448.                 'ProblemType',
  1449.                 'Date',
  1450.                 'Package',
  1451.                 'Field1',
  1452.                 'Field2',
  1453.                 'CommonField1',
  1454.                 'CommonField2']), 'report has required fields')
  1455.             self.assertEqual(r['Field1'], 'Field 1')
  1456.             self.assertEqual(r['Field2'], 'Field 2\nBla')
  1457.             self.assertEqual(r['CommonField1'], 'CommonField 1')
  1458.             self.assertEqual(r['CommonField2'], 'CommonField 2')
  1459.             r = Report()
  1460.             r['Package'] = 'foo 4.5-6'
  1461.             r.add_hooks_info()
  1462.             self.assertEqual(set(r.keys()), set([
  1463.                 'ProblemType',
  1464.                 'Date',
  1465.                 'Package',
  1466.                 'Field1',
  1467.                 'Field2',
  1468.                 'CommonField1',
  1469.                 'CommonField2']), 'report has required fields')
  1470.             self.assertEqual(r['Field1'], 'Field 1')
  1471.             self.assertEqual(r['Field2'], 'Field 2\nBla')
  1472.             self.assertEqual(r['CommonField1'], 'CommonField 1')
  1473.             self.assertEqual(r['CommonField2'], 'CommonField 2')
  1474.             open(os.path.join(_hook_dir, 'source_foo.py'), 'w').write("\ndef add_info(report):\n    report['Field1'] = 'Field 1'\n    report['Field2'] = 'Field 2\\nBla'\n")
  1475.             r = Report()
  1476.             r['SourcePackage'] = 'foo'
  1477.             r['Package'] = 'libfoo 3'
  1478.             r.add_hooks_info()
  1479.             self.assertEqual(set(r.keys()), set([
  1480.                 'ProblemType',
  1481.                 'Date',
  1482.                 'Package',
  1483.                 'SourcePackage',
  1484.                 'Field1',
  1485.                 'Field2',
  1486.                 'CommonField1',
  1487.                 'CommonField2']), 'report has required fields')
  1488.             self.assertEqual(r['Field1'], 'Field 1')
  1489.             self.assertEqual(r['Field2'], 'Field 2\nBla')
  1490.             self.assertEqual(r['CommonField1'], 'CommonField 1')
  1491.             self.assertEqual(r['CommonField2'], 'CommonField 2')
  1492.         finally:
  1493.             shutil.rmtree(_hook_dir)
  1494.             shutil.rmtree(_common_hook_dir)
  1495.             _hook_dir = orig_hook_dir
  1496.             _common_hook_dir = orig_common_hook_dir
  1497.  
  1498.  
  1499.     
  1500.     def test_ignoring(self):
  1501.         '''mark_ignore() and check_ignored().'''
  1502.         global _ignore_file, _ignore_file
  1503.         orig_ignore_file = _ignore_file
  1504.         workdir = tempfile.mkdtemp()
  1505.         _ignore_file = os.path.join(workdir, 'ignore.xml')
  1506.         
  1507.         try:
  1508.             open(os.path.join(workdir, 'bash'), 'w').write('bash')
  1509.             open(os.path.join(workdir, 'crap'), 'w').write('crap')
  1510.             bash_rep = Report()
  1511.             bash_rep['ExecutablePath'] = os.path.join(workdir, 'bash')
  1512.             crap_rep = Report()
  1513.             crap_rep['ExecutablePath'] = os.path.join(workdir, 'crap')
  1514.             cp_rep = Report()
  1515.             cp_rep['ExecutablePath'] = os.path.join(workdir, 'cp')
  1516.             self.assertEqual(bash_rep.check_ignored(), False)
  1517.             self.assertEqual(crap_rep.check_ignored(), False)
  1518.             self.assertEqual(cp_rep.check_ignored(), False)
  1519.             crap_rep.mark_ignore()
  1520.             self.assertEqual(bash_rep.check_ignored(), False)
  1521.             self.assertEqual(crap_rep.check_ignored(), True)
  1522.             self.assertEqual(cp_rep.check_ignored(), False)
  1523.             bash_rep.mark_ignore()
  1524.             self.assertEqual(bash_rep.check_ignored(), True)
  1525.             self.assertEqual(crap_rep.check_ignored(), True)
  1526.             self.assertEqual(cp_rep.check_ignored(), False)
  1527.             time.sleep(1)
  1528.             open(os.path.join(workdir, 'crap'), 'w').write('crapnew')
  1529.             self.assertEqual(bash_rep.check_ignored(), True)
  1530.             self.assertEqual(crap_rep.check_ignored(), False)
  1531.             self.assertEqual(cp_rep.check_ignored(), False)
  1532.             open(_ignore_file, 'w').write('')
  1533.             self.assertEqual(bash_rep.check_ignored(), False)
  1534.             self.assertEqual(crap_rep.check_ignored(), False)
  1535.             self.assertEqual(cp_rep.check_ignored(), False)
  1536.         finally:
  1537.             shutil.rmtree(workdir)
  1538.             _ignore_file = orig_ignore_file
  1539.  
  1540.  
  1541.     
  1542.     def test_blacklisting(self):
  1543.         '''check_ignored() for system-wise blacklist.'''
  1544.         global _blacklist_dir, _ignore_file, _blacklist_dir, _ignore_file
  1545.         orig_blacklist_dir = _blacklist_dir
  1546.         _blacklist_dir = tempfile.mkdtemp()
  1547.         orig_ignore_file = _ignore_file
  1548.         _ignore_file = '/nonexistant'
  1549.         
  1550.         try:
  1551.             bash_rep = Report()
  1552.             bash_rep['ExecutablePath'] = '/bin/bash'
  1553.             crap_rep = Report()
  1554.             crap_rep['ExecutablePath'] = '/bin/crap'
  1555.             self.assertEqual(bash_rep.check_ignored(), False)
  1556.             self.assertEqual(crap_rep.check_ignored(), False)
  1557.             open(os.path.join(_blacklist_dir, 'README'), 'w').write('# Ignore file\n#/bin/bash\n')
  1558.             open(os.path.join(_blacklist_dir, 'bl1'), 'w').write('/bin/bas\n/bin/bashh\nbash\nbin/bash\n')
  1559.             self.assertEqual(bash_rep.check_ignored(), False)
  1560.             self.assertEqual(crap_rep.check_ignored(), False)
  1561.             open(os.path.join(_blacklist_dir, 'bl_2'), 'w').write('/bin/crap\n')
  1562.             self.assertEqual(bash_rep.check_ignored(), False)
  1563.             self.assertEqual(crap_rep.check_ignored(), True)
  1564.             open(os.path.join(_blacklist_dir, 'bl1'), 'a').write('/bin/bash\n')
  1565.             self.assertEqual(bash_rep.check_ignored(), True)
  1566.             self.assertEqual(crap_rep.check_ignored(), True)
  1567.         finally:
  1568.             shutil.rmtree(_blacklist_dir)
  1569.             _blacklist_dir = orig_blacklist_dir
  1570.             _ignore_file = orig_ignore_file
  1571.  
  1572.  
  1573.     
  1574.     def test_has_useful_stacktrace(self):
  1575.         '''has_useful_stacktrace().'''
  1576.         r = Report()
  1577.         self.failIf(r.has_useful_stacktrace())
  1578.         r['StacktraceTop'] = ''
  1579.         self.failIf(r.has_useful_stacktrace())
  1580.         r['StacktraceTop'] = '?? ()'
  1581.         self.failIf(r.has_useful_stacktrace())
  1582.         r['StacktraceTop'] = '?? ()\n?? ()'
  1583.         self.failIf(r.has_useful_stacktrace())
  1584.         r['StacktraceTop'] = 'read () from /lib/libc.6.so\n?? ()'
  1585.         self.failIf(r.has_useful_stacktrace())
  1586.         r['StacktraceTop'] = 'read () from /lib/libc.6.so\n?? ()\n?? ()\n?? ()'
  1587.         self.failIf(r.has_useful_stacktrace())
  1588.         r['StacktraceTop'] = 'read () from /lib/libc.6.so\nfoo (i=1) from /usr/lib/libfoo.so'
  1589.         self.assert_(r.has_useful_stacktrace())
  1590.         r['StacktraceTop'] = 'read () from /lib/libc.6.so\nfoo (i=1) from /usr/lib/libfoo.so\n?? ()'
  1591.         self.assert_(r.has_useful_stacktrace())
  1592.         r['StacktraceTop'] = 'read () from /lib/libc.6.so\nfoo (i=1) from /usr/lib/libfoo.so\n?? ()\n?? ()'
  1593.         self.assert_(r.has_useful_stacktrace())
  1594.         r['StacktraceTop'] = 'read () from /lib/libc.6.so\n?? ()\nfoo (i=1) from /usr/lib/libfoo.so\n?? ()\n?? ()'
  1595.         self.failIf(r.has_useful_stacktrace())
  1596.  
  1597.     
  1598.     def test_standard_title(self):
  1599.         '''standard_title().'''
  1600.         report = Report()
  1601.         self.assertEqual(report.standard_title(), None)
  1602.         report['Signal'] = '11'
  1603.         report['ExecutablePath'] = '/bin/bash'
  1604.         report['StacktraceTop'] = 'foo()\nbar(x=3)\nbaz()\n'
  1605.         self.assertEqual(report.standard_title(), 'bash crashed with SIGSEGV in foo()')
  1606.         report['Signal'] = '42'
  1607.         self.assertEqual(report.standard_title(), 'bash crashed with signal 42 in foo()')
  1608.         report['StacktraceTop'] = ''
  1609.         self.assertEqual(report.standard_title(), 'bash crashed with signal 42')
  1610.         report['StacktraceTop'] = '??()\nfoo()'
  1611.         self.assertEqual(report.standard_title(), 'bash crashed with signal 42 in foo()')
  1612.         report['StacktraceTop'] = '??()\n??()'
  1613.         self.assertEqual(report.standard_title(), 'bash crashed with signal 42')
  1614.         report = Report()
  1615.         report['ExecutablePath'] = '/usr/share/apport/apport-gtk'
  1616.         report['Traceback'] = 'Traceback (most recent call last):\nFile "/usr/share/apport/apport-gtk", line 202, in <module>\napp.run_argv()\nFile "/var/lib/python-support/python2.5/apport/ui.py", line 161, in run_argv\nself.run_crashes()\nFile "/var/lib/python-support/python2.5/apport/ui.py", line 104, in run_crashes\nself.run_crash(f)\nFile "/var/lib/python-support/python2.5/apport/ui.py", line 115, in run_crash\nresponse = self.ui_present_crash(desktop_entry)\nFile "/usr/share/apport/apport-gtk", line 67, in ui_present_crash\nsubprocess.call([\'pgrep\', \'-x\',\nNameError: global name \'subprocess\' is not defined'
  1617.         self.assertEqual(report.standard_title(), 'apport-gtk crashed with NameError in ui_present_crash()')
  1618.         report = Report()
  1619.         report['ExecutablePath'] = '/usr/share/apport/apport-gtk'
  1620.         report['Traceback'] = 'TypeError: Cannot create a consistent method resolution\norder (MRO) for bases GObject, CanvasGroupableIface, CanvasGroupable'
  1621.         self.assertEqual(report.standard_title(), 'apport-gtk crashed with TypeError: Cannot create a consistent method resolution')
  1622.         report = Report()
  1623.         report['ExecutablePath'] = '/usr/share/apport/apport-gtk'
  1624.         report['Traceback'] = 'Traceback (most recent call last):\n  File "/x/foo.py", line 242, in setup_chooser\n    raise "Moo"\nMoo'
  1625.         self.assertEqual(report.standard_title(), 'apport-gtk crashed with Moo in setup_chooser()')
  1626.         report = Report()
  1627.         report['ExecutablePath'] = '/usr/share/apport/apport-gtk'
  1628.         report['Traceback'] = 'Traceback (most recent call last):\n  File "/x/foo.py", line 242, in setup_chooser\n    raise "\nKey: "+key+" isn\'t set.\nRestarting AWN usually solves this issue\n"\n \nKey: /apps/avant-window-navigator/app/active_png isn\'t set.\nRestarting AWN usually solves this issue'
  1629.         t = report.standard_title()
  1630.         self.assert_(t.startswith('apport-gtk crashed with'))
  1631.         self.assert_(t.endswith('setup_chooser()'))
  1632.         report = Report('Package')
  1633.         report['Package'] = 'bash'
  1634.         self.assertEqual(report.standard_title(), 'package bash failed to install/upgrade')
  1635.         report['ErrorMessage'] = ''
  1636.         self.assertEqual(report.standard_title(), 'package bash failed to install/upgrade')
  1637.         report['ErrorMessage'] = 'botched\nnot found\n'
  1638.         self.assertEqual(report.standard_title(), 'package bash failed to install/upgrade: not found')
  1639.         report['Signal'] = '11'
  1640.         report['ExecutablePath'] = '/bin/bash'
  1641.         report['StacktraceTop'] = 'foo()\nbar(x=3)\nbaz()\n'
  1642.         report['PackageArchitecture'] = 'amd64'
  1643.         report['Architecture'] = 'amd64'
  1644.         self.assertEqual(report.standard_title(), 'bash crashed with SIGSEGV in foo()')
  1645.         report['PackageArchitecture'] = 'i386'
  1646.         self.assertEqual(report.standard_title(), 'bash crashed with SIGSEGV in foo() [non-native i386 package]')
  1647.         report['PackageArchitecture'] = 'all'
  1648.         self.assertEqual(report.standard_title(), 'bash crashed with SIGSEGV in foo()')
  1649.         report = Report('KernelOops')
  1650.         report['OopsText'] = '------------[ cut here ]------------\nkernel BUG at /tmp/oops.c:5!\ninvalid opcode: 0000 [#1] SMP'
  1651.         self.assertEqual(report.standard_title(), 'kernel BUG at /tmp/oops.c:5!')
  1652.  
  1653.     
  1654.     def test_obsolete_packages(self):
  1655.         '''obsolete_packages().'''
  1656.         report = Report()
  1657.         self.assertRaises(KeyError, report.obsolete_packages)
  1658.         report['Package'] = 'bash 0'
  1659.         self.assertEqual(report.obsolete_packages(), [
  1660.             'bash'])
  1661.         report['Package'] = 'bash 0 [modified: /bin/bash]'
  1662.         self.assertEqual(report.obsolete_packages(), [
  1663.             'bash'])
  1664.         report['Package'] = 'bash ' + packaging.get_available_version('bash')
  1665.         self.assertEqual(report.obsolete_packages(), [])
  1666.         report['Dependencies'] = 'coreutils 0\ncron 0\n'
  1667.         self.assertEqual(report.obsolete_packages(), [
  1668.             'coreutils',
  1669.             'cron'])
  1670.         report['Dependencies'] = 'coreutils %s [modified: /bin/mount]\ncron 0\n' % packaging.get_available_version('coreutils')
  1671.         self.assertEqual(report.obsolete_packages(), [
  1672.             'cron'])
  1673.         report['Dependencies'] = 'coreutils %s\ncron %s\n' % (packaging.get_available_version('coreutils'), packaging.get_available_version('cron'))
  1674.         self.assertEqual(report.obsolete_packages(), [])
  1675.  
  1676.     
  1677.     def test_gen_stacktrace_top(self):
  1678.         '''_gen_stacktrace_top().'''
  1679.         r = Report()
  1680.         r['Stacktrace'] = '#0  0x10000488 in h (p=0x0) at crash.c:25\n#1  0x100004c8 in g (x=1, y=42) at crash.c:26\n#2  0x10000514 in f (x=1) at crash.c:27\n#3  0x10000530 in e (x=1) at crash.c:28\n#4  0x10000530 in d (x=1) at crash.c:29\n#5  0x10000530 in c (x=1) at crash.c:30\n#6  0x10000550 in main () at crash.c:31\n'
  1681.         r._gen_stacktrace_top()
  1682.         self.assertEqual(r['StacktraceTop'], 'h (p=0x0) at crash.c:25\ng (x=1, y=42) at crash.c:26\nf (x=1) at crash.c:27\ne (x=1) at crash.c:28\nd (x=1) at crash.c:29')
  1683.         r = Report()
  1684.         r['Stacktrace'] = '#0 h (p=0x0) at crash.c:25\n#1  0x100004c8 in g (x=1, y=42) at crash.c:26\n#2 f (x=1) at crash.c:27\n#3  0x10000530 in e (x=1) at crash.c:28\n#4  0x10000530 in d (x=1) at crash.c:29\n#5  0x10000530 in c (x=1) at crash.c:30\n#6  0x10000550 in main () at crash.c:31\n'
  1685.         r._gen_stacktrace_top()
  1686.         self.assertEqual(r['StacktraceTop'], 'h (p=0x0) at crash.c:25\ng (x=1, y=42) at crash.c:26\nf (x=1) at crash.c:27\ne (x=1) at crash.c:28\nd (x=1) at crash.c:29')
  1687.         r = Report()
  1688.         r['Stacktrace'] = '#0  0x10000488 in raise () from /lib/libpthread.so.0\n#1  0x100004c8 in ??\n#2  <signal handler called>\n#3  0x10000530 in e (x=1) at crash.c:28\n#4  0x10000530 in d (x=1) at crash.c:29\n#5  0x10000530 in c (x=1) at crash.c:30\n#6  0x10000550 in main () at crash.c:31\n'
  1689.         r._gen_stacktrace_top()
  1690.         self.assertEqual(r['StacktraceTop'], 'e (x=1) at crash.c:28\nd (x=1) at crash.c:29\nc (x=1) at crash.c:30\nmain () at crash.c:31')
  1691.         r = Report()
  1692.         r['Stacktrace'] = '#0  0x10000488 in raise () from /lib/libpthread.so.0\n#1  ??\n#2  <signal handler called>\n#3  0x10000530 in e (x=1) at crash.c:28\n#4  d (x=1) at crash.c:29\n#5  0x10000530 in c (x=1) at crash.c:30\n#6  0x10000550 in main () at crash.c:31\n'
  1693.         r._gen_stacktrace_top()
  1694.         self.assertEqual(r['StacktraceTop'], 'e (x=1) at crash.c:28\nd (x=1) at crash.c:29\nc (x=1) at crash.c:30\nmain () at crash.c:31')
  1695.         r = Report()
  1696.         r['Stacktrace'] = '#0  0x10000488 in raise () from /lib/libpthread.so.0\n#1  0x100004c8 in ??\n#2  <signal handler called>\n#3  0x10000530 in e (x=1) at crash.c:28\n#4  0x10000530 in d (x=1) at crash.c:29\n#5  0x10000123 in raise () from /lib/libpthread.so.0\n#6  <signal handler called>\n#7  0x10000530 in c (x=1) at crash.c:30\n#8  0x10000550 in main () at crash.c:31\n'
  1697.         r._gen_stacktrace_top()
  1698.         self.assertEqual(r['StacktraceTop'], 'e (x=1) at crash.c:28\nd (x=1) at crash.c:29\nraise () from /lib/libpthread.so.0\n<signal handler called>\nc (x=1) at crash.c:30')
  1699.         r = Report()
  1700.         r['Stacktrace'] = '#0  0xb7d39cab in IA__g_logv (log_domain=<value optimized out>, log_level=G_LOG_LEVEL_ERROR, \n    format=0xb7d825f0 "file %s: line %d (%s): assertion failed: (%s)", args1=0xbfee8e3c "xxx") at /build/buildd/glib2.0-2.13.5/glib/gmessages.c:493\n#1  0xb7d39f29 in IA__g_log (log_domain=0xb7edbfd0 "libgnomevfs", log_level=G_LOG_LEVEL_ERROR, \n    format=0xb7d825f0 "file %s: line %d (%s): assertion failed: (%s)") at /build/buildd/glib2.0-2.13.5/glib/gmessages.c:517\n#2  0xb7d39fa6 in IA__g_assert_warning (log_domain=0xb7edbfd0 "libgnomevfs", file=0xb7ee1a26 "gnome-vfs-volume.c", line=254, \n    pretty_function=0xb7ee1920 "gnome_vfs_volume_unset_drive_private", expression=0xb7ee1a39 "volume->priv->drive == drive")\n    at /build/buildd/glib2.0-2.13.5/glib/gmessages.c:552\nNo locals.\n#3  0xb7ec6c11 in gnome_vfs_volume_unset_drive_private (volume=0x8081a30, drive=0x8078f00) at gnome-vfs-volume.c:254\n        __PRETTY_FUNCTION__ = "gnome_vfs_volume_unset_drive_private"\n#4  0x08054db8 in _gnome_vfs_volume_monitor_disconnected (volume_monitor=0x8070400, drive=0x8078f00) at gnome-vfs-volume-monitor.c:963\n        vol_list = (GList *) 0x8096d30\n        current_vol = (GList *) 0x8097470\n#5  0x0805951e in _hal_device_removed (hal_ctx=0x8074da8, udi=0x8093be4 "/org/freedesktop/Hal/devices/volume_uuid_92FC9DFBFC9DDA35")\n    at gnome-vfs-hal-mounts.c:1316\n        backing_udi = <value optimized out>\n#6  0xb7ef1ead in filter_func (connection=0x8075288, message=0x80768d8, user_data=0x8074da8) at libhal.c:820\n        udi = <value optimized out>\n        object_path = 0x8076d40 "/org/freedesktop/Hal/Manager"\n        error = {name = 0x0, message = 0x0, dummy1 = 1, dummy2 = 0, dummy3 = 0, dummy4 = 1, dummy5 = 0, padding1 = 0xb7e50c00}\n#7  0xb7e071d2 in dbus_connection_dispatch (connection=0x8075288) at dbus-connection.c:4267\n#8  0xb7e33dfd in ?? () from /usr/lib/libdbus-glib-1.so.2'
  1701.         r._gen_stacktrace_top()
  1702.         self.assertEqual(r['StacktraceTop'], 'gnome_vfs_volume_unset_drive_private (volume=0x8081a30, drive=0x8078f00) at gnome-vfs-volume.c:254\n_gnome_vfs_volume_monitor_disconnected (volume_monitor=0x8070400, drive=0x8078f00) at gnome-vfs-volume-monitor.c:963\n_hal_device_removed (hal_ctx=0x8074da8, udi=0x8093be4 "/org/freedesktop/Hal/devices/volume_uuid_92FC9DFBFC9DDA35")\nfilter_func (connection=0x8075288, message=0x80768d8, user_data=0x8074da8) at libhal.c:820\ndbus_connection_dispatch (connection=0x8075288) at dbus-connection.c:4267')
  1703.  
  1704.     
  1705.     def test_crash_signature(self):
  1706.         '''crash_signature().'''
  1707.         r = Report()
  1708.         self.assertEqual(r.crash_signature(), None)
  1709.         r['Signal'] = '42'
  1710.         r['ExecutablePath'] = '/bin/crash'
  1711.         r['StacktraceTop'] = 'foo_bar (x=1) at crash.c:28\nd01 (x=1) at crash.c:29\nraise () from /lib/libpthread.so.0\n<signal handler called>\n__frob::~frob (x=1) at crash.c:30'
  1712.         self.assertEqual(r.crash_signature(), '/bin/crash:42:foo_bar:d01:raise:<signal handler called>:__frob::~frob')
  1713.         r['StacktraceTop'] = 'foo_bar (x=1) at crash.c:28\n??\nraise () from /lib/libpthread.so.0\n<signal handler called>\n__frob (x=1) at crash.c:30'
  1714.         self.assertEqual(r.crash_signature(), None)
  1715.         del r['Signal']
  1716.         r['Traceback'] = 'Traceback (most recent call last):\n  File "test.py", line 7, in <module>\n    print _f(5)\n  File "test.py", line 5, in _f\n    return g_foo00(x+1)\n  File "test.py", line 2, in g_foo00\n    return x/0\nZeroDivisionError: integer division or modulo by zero'
  1717.         self.assertEqual(r.crash_signature(), '/bin/crash:ZeroDivisionError:<module>:_f:g_foo00')
  1718.         r['Traceback'] = 'TypeError: function takes exactly 0 arguments (1 given)'
  1719.         self.assertEqual(r.crash_signature(), '/bin/crash:TypeError')
  1720.         r['Traceback'] = 'FooBar'
  1721.         self.assertEqual(r.crash_signature(), None)
  1722.  
  1723.     
  1724.     def test_binary_data(self):
  1725.         '''methods get along with binary data.'''
  1726.         pr = Report()
  1727.         pr['Signal'] = '11'
  1728.         pr['ExecutablePath'] = '/bin/foo'
  1729.         pr['Stacktrace'] = '#0  0x10000488 in h (p="\x00\x00\x00\x01\x02") at crash.c:25\n#1  0x10000550 in main () at crash.c:31\n'
  1730.         pr['ThreadStacktrace'] = pr['Stacktrace']
  1731.         pr['ProcCmdline'] = 'python\x00-OO\t\x00/bin/bash'
  1732.         pr._gen_stacktrace_top()
  1733.         io = StringIO()
  1734.         pr.write(io)
  1735.         io.seek(0)
  1736.         pr = Report()
  1737.         pr.load(io, binary = 'compressed')
  1738.         if not hasattr(pr['StacktraceTop'], 'get_value'):
  1739.             raise AssertionError
  1740.         self.assertEqual(pr.has_useful_stacktrace(), True)
  1741.         self.assertEqual(pr.crash_signature(), '/bin/foo:11:h:main')
  1742.         self.assertEqual(pr.standard_title(), 'foo crashed with SIGSEGV in h()')
  1743.  
  1744.     
  1745.     def test_module_license_evaluation(self):
  1746.         '''module licenses can be validated correctly.'''
  1747.         
  1748.         def _build_ko(license):
  1749.             asm = tempfile.NamedTemporaryFile(prefix = '%s-' % license, suffix = '.S')
  1750.             asm.write('.section .modinfo\n.string "license=%s"\n' % license)
  1751.             asm.flush()
  1752.             ko = tempfile.NamedTemporaryFile(prefix = '%s-' % license, suffix = '.ko')
  1753.             subprocess.call([
  1754.                 '/usr/bin/as',
  1755.                 asm.name,
  1756.                 '-o',
  1757.                 ko.name])
  1758.             return ko
  1759.  
  1760.         good_ko = _build_ko('GPL')
  1761.         bad_ko = _build_ko('BAD')
  1762.         self.assert_('GPL' in get_module_license('isofs'))
  1763.         self.assert_(get_module_license('does-not-exist') == None)
  1764.         self.assert_('GPL' in get_module_license(good_ko.name))
  1765.         self.assert_('BAD' in get_module_license(bad_ko.name))
  1766.         f = tempfile.NamedTemporaryFile()
  1767.         f.write('isofs\ndoes-not-exist\n%s\n%s\n' % (good_ko.name, bad_ko.name))
  1768.         f.flush()
  1769.         nonfree = nonfree_modules(f.name)
  1770.         self.failIf('isofs' in nonfree)
  1771.         self.failIf('does-not-exist' in nonfree)
  1772.         self.failIf(good_ko.name in nonfree)
  1773.         self.assert_(bad_ko.name in nonfree)
  1774.  
  1775.  
  1776. if __name__ == '__main__':
  1777.     unittest.main()
  1778.  
  1779.